﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Caching;

using Pg.BioMedics.SDR.Caching.Linq;

namespace Pg.BioMedics.SDR.Caching
{
    /// <summary>
    /// Caching strategy using Microsoft Distributed Cache as a storage for 
    /// cached entities.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    internal class DistributedCachingStrategy<T> : ICachingStrategy<T>
    {
        #region Constants

        internal const string TagSeparator = ":";

        #endregion

        #region Fields

        private readonly Cache cache;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates instance of the DistributedCachingRegion for the distributed cache
        /// defined by ist name.
        /// </summary>
        /// <param name="cacheName">Distributed cache name</param>
        internal DistributedCachingStrategy(string cacheName)
        {
            CacheFactory factory = new CacheFactory();
            this.cache = factory.GetCache(String.IsNullOrEmpty(cacheName) ? null : cacheName);

            TryCreateRegion();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets distiributed cache region managed by this strategy
        /// </summary>
        internal string Region
        {
            get
            {
                return typeof(T).Name;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets item from the cache basing on the parameter. If parameter follows 
        /// a tag pattern, entity will be retrived by this tag value. Otherwise, 
        /// it will be retrived by key.
        /// </summary>
        /// <param name="parameter">Cache query parameter</param>
        /// <returns>Collection of items retrived from the cache</returns>
        internal IEnumerable<T> Get(string parameter)
        {
            // input params validation
            if (String.IsNullOrEmpty(parameter))
                throw new CachingException("Couldnot retrive entity from the cache, paramter cannot be null or empty string");

            try
            {
                if (!IsTagParameter(parameter))
                {
                    if (!Exists(parameter))
                        return new List<T>();

                    CacheItem cacheItem = this.cache.GetCacheItem(Region, parameter);
                    return new List<T>() { (T)cacheItem.CacheObject };
                }
                else
                    return (from tagResult in this.cache.GetByTag(Region, new Tag(parameter))
                            select (T)tagResult.Value);
            }
            catch (Exception ex)
            {
                throw new CachingException("Couldnot retrive item {0} from cache by parameter {1}", ex, typeof(T).Name, parameter);
            }
        }

        /// <summary>
        /// Gets collection of items from cahce basing on tag paramters
        /// </summary>
        /// <param name="parameters">List of tag paramters</param>
        /// <returns>Resulting collection</returns>
        internal IEnumerable<T> Get(string[] parameters)
        {
            // input params validation
            if (parameters == null)
                throw new CachingException("Couldnot get items from cache, paramters collection cannot be null");
            else if (parameters.Length == 0)
                throw new CachingException("Couldnot get items from cache, paramters collection cannot be empty");

            try
            {
                Tag[] tags = (from parameter in parameters
                              where IsTagParameter(parameter)
                              select new Tag(parameter)).ToArray();
                if (tags.Length != parameters.Length)
                    throw new InvalidOperationException("All paramters should have tag parameter structure");

                return (from tagResult in this.cache.GetAllMatchingTags(Region, tags)
                        select (T)tagResult.Value);
            }
            catch (Exception ex)
            {
                throw new CachingException("Couldnot retrive item {0} from cache by parameters", ex, typeof(T).Name);
            }
        }

        #endregion

        #region ICachingStrategy<T> Members

        /// <summary>
        /// Puts an item into the distributed cache managed by this strategy. If item already 
        /// exists it will be replaced by the specified.
        /// </summary>
        /// <param name="key">Items key</param>
        /// <param name="item">Item instance</param>
        public void Put(string key, T item)
        {
            // input params validation
            if (item == null)
                throw new CachingException("Couldnot put an item of type {0} into the cache, the item cannot be null", typeof(T));

            if (Exists(key))
                cache.Remove(Region, key);

            this.cache.Add(Region, key, item, CreateTags(item));
        }

        /// <summary>
        /// Checks if cache already contains item defined by the key
        /// </summary>
        /// <param name="key">Items key</param>
        /// <returns>true - if item defined by the key already exists</returns>
        public bool Contains(string key)
        {
            return Exists(key);
        }

        /// <summary>
        /// Removes an item from the distributed cache
        /// </summary>
        /// <param name="key">Items key</param>
        public void Remove(string key)
        {
            this.cache.Remove(Region, key);
        }

        /// <summary>
        /// Removes all items from the distributed cache
        /// </summary>
        public void Clear()
        {
            this.cache.ClearRegion(Region);
        }

        /// <summary>
        /// Gets queriable interface associated to this strategy
        /// </summary>
        /// <returns></returns>
        public IQueryable<T> GetQueryable()
        {
            return new DistributedCacheQueryable<T>(this);
        }

        #endregion

        #region Support methods

        /// <summary>
        /// Creates collection of Tags for given item. This method uses reflection
        /// for retriving list of properties and its values
        /// </summary>
        /// <param name="item">Item which will be inserted into the cache</param>
        /// <returns></returns>
        private Tag[] CreateTags(T item)
        {
            return (from property in item.GetType().GetProperties()
                    where (property.PropertyType == typeof(string)) ||
                          (property.PropertyType == typeof(int)) ||
                          (property.PropertyType == typeof(int?)) ||
                          (property.PropertyType == typeof(long)) ||
                          (property.PropertyType == typeof(long?)) ||
                          (property.PropertyType == typeof(double)) ||
                          (property.PropertyType == typeof(double?)) ||
                          (property.PropertyType == typeof(float)) ||
                          (property.PropertyType == typeof(float?)) ||
                          (property.PropertyType == typeof(decimal)) ||
                          (property.PropertyType == typeof(decimal?)) ||
                          (property.PropertyType == typeof(char)) ||
                          (property.PropertyType == typeof(char?)) ||
                          (property.PropertyType == typeof(Guid)) ||
                          (property.PropertyType == typeof(Guid?))
                    select new Tag(String.Format("{0}:{1}", property.Name, property.GetValue(item, null)))).ToArray();
        }

        /// <summary>
        /// Checks if the cache query paramter is a tag or key paramter.
        /// </summary>
        /// <param name="parameter">query paramter</param>
        /// <returns>true - if parameter is a tag parameter</returns>
        private bool IsTagParameter(string parameter)
        {
            return ((from property in typeof(T).GetProperties()
                     where ((property.PropertyType == typeof(string)) ||
                            (property.PropertyType == typeof(int)) ||
                            (property.PropertyType == typeof(int?)) ||
                            (property.PropertyType == typeof(long)) ||
                            (property.PropertyType == typeof(long?)) ||
                            (property.PropertyType == typeof(double)) ||
                            (property.PropertyType == typeof(double?)) ||
                            (property.PropertyType == typeof(float)) ||
                            (property.PropertyType == typeof(float?)) ||
                            (property.PropertyType == typeof(decimal)) ||
                            (property.PropertyType == typeof(decimal?)) ||
                            (property.PropertyType == typeof(char)) ||
                            (property.PropertyType == typeof(char?)) ||
                            (property.PropertyType == typeof(Guid)) ||
                            (property.PropertyType == typeof(Guid?))) &&
                           (parameter.StartsWith(property.Name + TagSeparator))
                     select property).Count() > 0);
        }

        /// <summary>
        /// Tries to create a new region in the cache. If region already exists, does nothing.
        /// </summary>
        private void TryCreateRegion()
        {
            try
            {
                this.cache.CreateRegion(Region, false);
            }
            catch { }
        }

        /// <summary>
        /// Checks if given key already exists in currently used cache region
        /// </summary>
        /// <param name="key">Key</param>
        /// <returns>true - if exists</returns>
        private bool Exists(string key)
        {
            try
            {
                this.cache.GetCacheItem(Region, key);
                return true;
            }
            catch (CacheException ce)
            {
                if (ce.ErrorCode == DistributedCacheErrors.KeyDoesNotExist)
                    return false;
                else
                    throw new CachingException("Couldnot chack if item exists {0}", ce, key);
            }
            catch (Exception ex)
            {
                throw new CachingException("Couldnot chack if item exists {0}", ex, key);
            }
        }

        #endregion 
    }
}
